/*
* ProActive Parallel Suite(TM):
* The Open Source library for parallel and distributed
* Workflows & Scheduling, Orchestration, Cloud Automation
* and Big Data Analysis on Enterprise Grids & Clouds.
*
* Copyright (c) 2007 - 2017 ActiveEon
* Contact: contact@activeeon.com
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation: version 3 of
* the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* If needed, contact us to obtain a release under GPL Version 2 or 3
* or a different license than the AGPL.
*/
package org.ow2.proactive.authentication;
import java.util.Hashtable;
import java.util.Map;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import org.apache.log4j.Logger;
import org.ow2.proactive.authentication.principals.GroupNamePrincipal;
import org.ow2.proactive.authentication.principals.UserNamePrincipal;
/**
* Authentication based on LDAP system.
*
* Improved version of @see{LDAPLoginModule}
*
* support custom filters for username and group
*
*
* @author The ActiveEon Team
* @since ProActive Scheduling 2.1.1
*/
public abstract class LDAPLoginModule extends FileLoginModule implements Loggable {
/** connection logger */
private final Logger logger = getLogger();
/** LDAP configuration properties */
private LDAPProperties ldapProperties = new LDAPProperties(getLDAPConfigFileName());
/** default value for Context.SECURITY_AUTHENTICATION
* that correspond to anonymous connection
*/
private final String ANONYMOUS_LDAP_CONNECTION = "none";
/** name of key store path java property */
private final String SSL_KEYSTORE_PATH_PROPERTY = "javax.net.ssl.keyStore";
/** name of key store password java property */
private final String SSL_KEYSTORE_PASSWD_PROPERTY = "javax.net.ssl.keyStorePassword";
/** name of trust store password java property */
private final String SSL_TRUSTSTORE_PATH_PROPERTY = "javax.net.ssl.trustStore";
/** name of trust store password java property */
private final String SSL_TRUSTSTORE_PASSWD_PROPERTY = "javax.net.ssl.trustStorePassword";
/** boolean defining whether connection polling has to be used */
private final String LDAP_CONNECTION_POOLING = ldapProperties.getProperty(LDAPProperties.LDAP_CONNECTION_POOLING);
/** LDAP used to perform authentication */
private final String LDAP_URL = ldapProperties.getProperty(LDAPProperties.LDAP_URL);
/** LDAP Subtree wherein users entries are searched */
private final String USERS_DN = ldapProperties.getProperty(LDAPProperties.LDAP_USERS_SUBTREE);
/**
* LDAP Subtree wherein groups entries are searched
* If empty, then USERS_DN is used instead
*/
private String GROUPS_DN = ldapProperties.getProperty(LDAPProperties.LDAP_GROUPS_SUBTREE);
/**
* Authentication method used to bind to LDAP: none, simple,
* or one of the SASL authentication methods
*/
private final String AUTHENTICATION_METHOD = ldapProperties.getProperty(LDAPProperties.LDAP_AUTHENTICATION_METHOD);
/** user name used to bind to LDAP (if authentication method is different from none) */
private final String BIND_LOGIN = ldapProperties.getProperty(LDAPProperties.LDAP_BIND_LOGIN);
/** user password used to bind to LDAP (if authentication method is different from none) */
private String BIND_PASSWD = ldapProperties.getProperty(LDAPProperties.LDAP_BIND_PASSWD);
/**fall back property, check user/password and group in files if user in not found in LDAP */
private boolean fallbackUserAuth = Boolean.valueOf(ldapProperties.getProperty(LDAPProperties.FALLBACK_USER_AUTH));
/**group fall back property, check user group membership group file if user in not found in corresponding LDAP group*/
private boolean fallbackGroupMembership = Boolean.valueOf(ldapProperties.getProperty(LDAPProperties.FALLBACK_GROUP_MEMBERSHIP));
/** authentication status */
private boolean succeeded = false;
/**
* Creates a new instance of LDAPLoginModule
*/
public LDAPLoginModule() {
if (GROUPS_DN == null) {
GROUPS_DN = USERS_DN;
}
if (fallbackUserAuth) {
checkLoginFile();
checkGroupFile();
logger.info("Using Login file for fall back authentication at: " + loginFile);
logger.info("Using Group file for fall back group membership at: " + groupFile);
} else if (fallbackGroupMembership) {
checkGroupFile();
logger.info("Using Group file for fall back group membership at: " + groupFile);
}
//initialize system properties for SSL/TLS connection
String keyStore = ldapProperties.getProperty(LDAPProperties.LDAP_KEYSTORE_PATH);
if ((keyStore != null) && (!alreadyDefined(SSL_KEYSTORE_PATH_PROPERTY, keyStore))) {
System.setProperty(SSL_KEYSTORE_PATH_PROPERTY, keyStore);
System.setProperty(SSL_KEYSTORE_PASSWD_PROPERTY,
ldapProperties.getProperty(LDAPProperties.LDAP_KEYSTORE_PASSWD));
}
String trustStore = ldapProperties.getProperty(LDAPProperties.LDAP_TRUSTSTORE_PATH);
if ((trustStore != null) && (!alreadyDefined(SSL_TRUSTSTORE_PATH_PROPERTY, trustStore))) {
System.setProperty(SSL_TRUSTSTORE_PATH_PROPERTY, trustStore);
System.setProperty(SSL_TRUSTSTORE_PASSWD_PROPERTY,
ldapProperties.getProperty(LDAPProperties.LDAP_TRUSTSTORE_PASSWD));
}
}
/**
* Checks if property is already defined.
*
* @param propertyName name of the property
* @param propertyValue value of the property
* @return true id the property is defined and its value equals the specified value.
*/
private boolean alreadyDefined(String propertyName, String propertyValue) {
if (propertyName != null && propertyName.length() != 0) {
String definedPropertyValue = System.getProperty(propertyName);
if (System.getProperty(propertyName) != null && !definedPropertyValue.equals(propertyValue)) {
logger.warn("Property " + propertyName + " is already defined");
logger.warn("Using old value " + propertyValue);
return true;
}
}
return false;
}
/**
* Initialize this <code>LDAPLoginModule</code>.
*
* <p>
*
* @param subject
* the <code>Subject</code> not to be authenticated.
* <p>
*
* @param callbackHandler
* a <code>CallbackHandler</code> to get the credentials of the
* user, must work with <code>NoCallback</code> callbacks.
* <p>
* @param sharedState state shared with other configured LoginModules. <p>
*
* @param options options specified in the login
* <code>Configuration</code> for this particular
* <code>LDAPLoginModule</code>.
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
if (logger.isDebugEnabled()) {
logger.debug("Using LDAP: " + LDAP_URL);
}
}
/**
* Authenticate the user by getting the user name and password from the
* CallbackHandler.
*
* <p>
*
* @return true in all cases since this <code>LDAPLoginModule</code>
* should not be ignored.
*
* @exception FailedLoginException
* if the authentication fails.
* <p>
*
* @exception LoginException
* if this <code>LDAPLoginModule</code> is unable to
* perform the authentication.
*/
@Override
public boolean login() throws LoginException {
succeeded = false;
if (callbackHandler == null) {
throw new LoginException("Error: no CallbackHandler available " +
"to garner authentication information from the user");
}
try {
Callback[] callbacks = new Callback[] { new NoCallback() };
// gets the user name, password, group Membership, and group Hierarchy from call back handler
callbackHandler.handle(callbacks);
Map<String, Object> params = ((NoCallback) callbacks[0]).get();
String username = (String) params.get("username");
String password = (String) params.get("pw");
params.clear();
((NoCallback) callbacks[0]).clear();
if (username == null) {
logger.info("No username has been specified for authentication");
throw new FailedLoginException("No username has been specified for authentication");
}
succeeded = logUser(username, password);
return succeeded;
} catch (java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException("Error: " + uce.getCallback().toString() +
" not available to garner authentication information " + "from the user");
}
}
/**
* Check user and password from file, or authenticate with ldap.
*
* @param username user's login
* @param password user's password
* @return true user login and password are correct, and requested group is authorized for the user
* @throws LoginException if authentication and group membership fails.
*/
protected boolean logUser(String username, String password) throws LoginException {
if (fallbackUserAuth) {
try {
return super.logUser(username, password, false);
} catch (LoginException ex) {
return internalLogUser(username, password);
}
} else {
return internalLogUser(username, password);
}
}
private boolean internalLogUser(String username, String password) throws LoginException {
// check the user name, get the RDN of the user
// (null = not found)
String userDN = null;
boolean passwordMatch = false;
try {
userDN = getLDAPUserDN(username);
} catch (NamingException e) {
logger.error("Cannot connect to LDAP server", e);
throw new FailedLoginException("Cannot connect to LDAP server");
}
if (userDN == null) {
logger.info("user entry not found in subtree " + USERS_DN + " for user " + username);
throw new FailedLoginException("User name doesn't exists");
} else {
// Check if the password match the user name
passwordMatch = checkLDAPPassword(userDN, password);
}
if (passwordMatch) {
if (logger.isDebugEnabled()) {
logger.debug("authentication succeeded, checking group");
}
if (fallbackGroupMembership) {
super.groupMembershipFromFile(username);
}
} else {
// authentication failed
logger.info("password verification failed for user: " + username);
throw new FailedLoginException("Password Incorrect");
}
return true;
}
/**
* <p>
* This method is called if the LoginContext's overall authentication
* succeeded (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
* LoginModules succeeded).
* <p>
*
* @exception LoginException
* if the commit fails.
*
* @return true if this LDAPLoginModule's own login and commit attempts
* succeeded, or false otherwise.
*/
@Override
public boolean commit() throws LoginException {
return succeeded;
}
/**
* <p>
* This method is called if the LoginContext's overall authentication
* failed. (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
* LoginModules did not succeed).
*
* <p>
* If this LDAPLoginModule's own authentication attempt succeeded (checked
* by retrieving the private state saved by the <code>login</code> and
* <code>commit</code> methods), then this method cleans up any state that
* was originally saved.
*
* <p>
*
* @exception LoginException
* if the abort fails.
*
* @return false if this LoginModule's own login and/or commit attempts
* failed, and true otherwise.
*/
@Override
public boolean abort() throws LoginException {
boolean result = succeeded;
succeeded = false;
return result;
}
/**
* Logout the user.
* <p>
*
* @exception LoginException
* if the logout fails.
*
* @return true in all cases since this <code>LoginModule</code> should
* not be ignored.
*/
@Override
public boolean logout() throws LoginException {
succeeded = false;
return true;
}
/**
* Connect using SSL encryption to the LDAP server <code>url</code> and
* tries to authenticate with the <code>UID</code> and
* <code>password</code>.
*
* <p>
*
* @param userDN user name
* <p>
* @param password user password
* <p>
* @return true if the authentication is successful, false otherwise.
*/
private boolean checkLDAPPassword(String userDN, String password) {
if (logger.isDebugEnabled()) {
logger.debug("check password for user: " + userDN);
}
Hashtable<String, String> env = createBasicEnvForInitalContext();
env.put(Context.SECURITY_PRINCIPAL, userDN);
env.put(Context.SECURITY_CREDENTIALS, password);
DirContext ctx = null;
try {
// Create the initial directory context
ctx = new InitialDirContext(env);
} catch (NamingException e) {
logger.error("Problem checkin user password, user password may be wrong: " + e);
return false;
}
// Close the context when we're done
try {
ctx.close();
} catch (NamingException e) {
logger.error("Problem closing secure connection: " + e);
}
return true;
}
/**
* Connects anonymously to the LDAP server <code>url</code> and retrieve
* DN of the user <code>username</code>
*
* <p>
* @exception NamingException
* if a naming exception is encountered.
* <p>
*
* @return the String containing the UID of the user or null if the user is
* not found.
*/
private String getLDAPUserDN(String username) throws NamingException {
String userDN = null;
DirContext ctx = null;
try {
// Create the initial directory context
ctx = this.connectAndGetContext();
SearchControls sControl = new SearchControls();
sControl.setSearchScope(SearchControls.SUBTREE_SCOPE);
String filter = String.format(ldapProperties.getProperty(LDAPProperties.LDAP_USER_FILTER), username);
// looking for the user dn (distinguish name)
NamingEnumeration<SearchResult> answer = ctx.search(USERS_DN, filter, sControl);
if (answer.hasMoreElements()) {
SearchResult result = (SearchResult) answer.next();
userDN = result.getNameInNamespace();
if (logger.isDebugEnabled()) {
logger.debug("User " + username + " has LDAP entry " + userDN);
}
subject.getPrincipals().add(new UserNamePrincipal(username));
// looking for the user groups
String groupFilter = String.format(ldapProperties.getProperty(LDAPProperties.LDAP_GROUP_FILTER),
userDN);
NamingEnumeration<SearchResult> groupResults = ctx.search(GROUPS_DN, groupFilter, sControl);
while (groupResults.hasMoreElements()) {
SearchResult res = (SearchResult) groupResults.next();
Attribute attr = res.getAttributes()
.get(ldapProperties.getProperty(LDAPProperties.LDAP_GROUPNAME_ATTR));
if (attr != null) {
String groupName = attr.get().toString();
subject.getPrincipals().add(new GroupNamePrincipal(groupName));
if (logger.isDebugEnabled()) {
logger.debug("User " + username + " is a member of group " + groupName);
}
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("User DN not found");
}
}
} catch (NamingException e) {
logger.error("Problem with the search in mode: " + AUTHENTICATION_METHOD + e);
throw e;
} finally {
try {
if (ctx != null) {
ctx.close();
}
} catch (NamingException e) {
logger.error("", e);
logger.error("Problem closing LDAP connection: " + e.getMessage());
}
}
return userDN;
}
/**
* Performs connection to LDAP with appropriate security parameters
* @return directory service interface.
* @throws NamingException
*/
private DirContext connectAndGetContext() throws NamingException {
Hashtable<String, String> env = createBasicEnvForInitalContext();
if (!AUTHENTICATION_METHOD.equals(ANONYMOUS_LDAP_CONNECTION)) {
env.put(Context.SECURITY_PRINCIPAL, BIND_LOGIN);
env.put(Context.SECURITY_CREDENTIALS, BIND_PASSWD);
}
// Create the initial directory context
return new InitialDirContext(env);
}
/**
* Retrieves LDAP configuration file name.
*
* @return name of the file with LDAP configuration.
*/
protected abstract String getLDAPConfigFileName();
private Hashtable<String, String> createBasicEnvForInitalContext() {
Hashtable<String, String> env = new Hashtable<>(6, 1f);
env.put("com.sun.jndi.ldap.connect.pool", LDAP_CONNECTION_POOLING);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, AUTHENTICATION_METHOD);
env.put(Context.PROVIDER_URL, LDAP_URL);
return env;
}
}